﻿using System;
using System.Net;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using System.Diagnostics;

namespace VS.Infrastructure.Controls
{
    [TemplatePart(Name = InertialPagedScrollViewer.RootElementName, Type = typeof(Border))]
    [TemplatePart(Name = InertialPagedScrollViewer.CanvasElementName, Type = typeof(Canvas))]
    [TemplatePart(Name = InertialPagedScrollViewer.ContentElementName, Type = typeof(ContentControl))]
    [TemplatePart(Name = InertialPagedScrollViewer.EasingStoryboardName, Type = typeof(Storyboard))]
    public class InertialPagedScrollViewer : ContentControl
    {
        #region Template Parts

        private const string RootElementName = "RootElement";
        /// <summary>
        /// Gets or sets the root element.
        /// </summary>
        /// <value>The root element.</value>
        private FrameworkElement RootElement { get; set; }

        private const string CanvasElementName = "CanvasElement";
        /// <summary>
        /// Gets or sets the canvas element.
        /// </summary>
        /// <value>The canvas element.</value>
        private Canvas CanvasElement { get; set; }

        private const string ContentElementName = "ContentElement";
        /// <summary>
        /// Gets or sets the content element.
        /// </summary>
        /// <value>The content element.</value>
        private ContentControl ContentElement { get; set; }

        private const string EasingStoryboardName = "EasingStoryboard";
        /// <summary>
        /// Gets or sets the easing storyboard.
        /// </summary>
        /// <value>The easing storyboard.</value>
        private Storyboard EasingStoryboard { get; set; }

        /// <summary>
        /// Gets or sets the easing animation.
        /// </summary>
        /// <value>The easing animation.</value>
        private DoubleAnimation EasingAnimation { get; set; }

        /// <summary>
        /// Gets or sets the canvas clipping.
        /// </summary>
        /// <value>The canvas clipping.</value>
        private RectangleGeometry CanvasClipping { get; set; }

        /// <summary>
        /// Gets or sets the translate.
        /// </summary>
        /// <value>The translate.</value>
        private TranslateTransform Translate { get; set; }

        #endregion

        #region OrientationProperty

        public static readonly DependencyProperty OrientationProperty =
            DependencyProperty.Register(
                "Orientation",
                typeof(Orientation),
                typeof(InertialPagedScrollViewer),
                new PropertyMetadata(Orientation.Vertical));

        /// <summary>
        /// Gets or sets the orientation.
        /// </summary>
        /// <value>The orientation.</value>
        public Orientation Orientation
        {
            get { return (Orientation)GetValue(OrientationProperty); }
            set { SetValue(OrientationProperty, value); }
        }

        /// <summary>
        /// Maxes the speed property_ changed.
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="e">The <see cref="System.Windows.DependencyPropertyChangedEventArgs"/> instance containing the event data.</param>
        private static void OrientationProperty_Changed(DependencyObject sender, DependencyPropertyChangedEventArgs e)
        {
            InertialPagedScrollViewer control = sender as InertialPagedScrollViewer;

            if (control != null)
                control.OnOrientationChanged((Orientation)e.NewValue);
        }

        private void OnOrientationChanged(Orientation orientation)
        {
            if (this.EasingStoryboard != null)
            {
                Storyboard.SetTarget(this.EasingStoryboard, this.Translate);
                Storyboard.SetTargetProperty(
                    this.EasingStoryboard,
                    this.Orientation == Orientation.Horizontal ? new PropertyPath("X") : new PropertyPath("Y"));
            }
        }

        #endregion

        #region UsePagingProperty

        public bool UsePaging
        {
            get { return (bool)GetValue(UsePagingProperty); }
            set { SetValue(UsePagingProperty, value); }
        }

        public static readonly DependencyProperty UsePagingProperty =
            DependencyProperty.Register("UsePaging", typeof(bool), typeof(InertialPagedScrollViewer), new PropertyMetadata(true));

        #endregion

        #region PageIndexProperty

        private int _pageIndex;

        public int PageIndex
        {
            get { return (int)GetValue(PageIndexProperty); }
            set { SetValue(PageIndexProperty, value); }
        }

        public static readonly DependencyProperty PageIndexProperty =
            DependencyProperty.Register("PageIndex", typeof(int), typeof(InertialPagedScrollViewer), new PropertyMetadata(-1, new PropertyChangedCallback(PageIndexProperty_Changed)));

        private static void PageIndexProperty_Changed(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            if (e.NewValue != e.OldValue && (int)e.OldValue != -1)
            {
                InertialPagedScrollViewer c = d as InertialPagedScrollViewer;
                if (c != null)
                {

                    int newPageIndex = (int)e.NewValue;
                    int delta = c._pageIndex - newPageIndex;
                    if (c.ContentElement != null)                    
                        c.ScrollDelta(delta);                   
                    else
                        c._deferredDelta = delta;

                }
            }
        }
        
        #endregion

        #region PageCountProperty

        public int PageCount
        {
            get { return (int)GetValue(PageCountProperty); }
            set { SetValue(PageCountProperty, value); }
        }

        public static readonly DependencyProperty PageCountProperty =
            DependencyProperty.Register("PageCount", typeof(int), typeof(InertialPagedScrollViewer), new PropertyMetadata(0));

        #endregion

        #region PageLoopProperty

        public bool PageLoop
        {
            get { return (bool)GetValue(PageLoopProperty); }
            set { SetValue(PageLoopProperty, value); }
        }

        public static readonly DependencyProperty PageLoopProperty =
            DependencyProperty.Register("PageLoop", typeof(bool), typeof(InertialPagedScrollViewer), new PropertyMetadata(true));

        #endregion

        #region PageWidthProperty

        public double PageWidth
        {
            get { return (double)GetValue(PageWidthProperty); }
            set { SetValue(PageWidthProperty, value); }
        }

        // Using a DependencyProperty as the backing store for PageWidth.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty PageWidthProperty =
            DependencyProperty.Register("PageWidth", typeof(double), typeof(InertialPagedScrollViewer), new PropertyMetadata(0.0, new PropertyChangedCallback(PageWidthProperty_Changed)));

        private static void PageWidthProperty_Changed(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            if (e.NewValue != e.OldValue)
            {
                InertialPagedScrollViewer c = d as InertialPagedScrollViewer;
                if (c != null && c.CanvasElement != null)
                {
                    //c.CanvasElement.Width = (double)e.NewValue;
                }
            }
        }

        #endregion

        #region FrictionFactorProperty

        public static readonly DependencyProperty FrictionFactorProperty =
            DependencyProperty.Register(
                "FrictionFactor",
                typeof(double),
                typeof(InertialPagedScrollViewer),
                new PropertyMetadata(.4));

        /// <summary>
        /// Gets or sets the friction factor.
        /// </summary>
        /// <value>The friction factor.</value>
        public double FrictionFactor
        {
            get { return (double)GetValue(FrictionFactorProperty); }
            set { SetValue(FrictionFactorProperty, value); }
        }

        #endregion

        #region DecelerationProperty

        // Using a DependencyProperty as the backing store for Deceleration.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty DecelerationProperty =
            DependencyProperty.Register(
                "Deceleration",
                typeof(double),
                typeof(InertialPagedScrollViewer),
                new PropertyMetadata(1.5));

        /// <summary>
        /// Gets or sets the deceleration.
        /// </summary>
        /// <value>The deceleration.</value>
        public double Deceleration
        {
            get { return (double)GetValue(DecelerationProperty); }
            set { SetValue(DecelerationProperty, value); }
        }

        #endregion

        #region MaxSpeedProperty

        public static readonly DependencyProperty MaxSpeedProperty =
            DependencyProperty.Register(
                "MaxSpeed",
                typeof(double),
                typeof(InertialPagedScrollViewer),
                new PropertyMetadata(2000.0, new PropertyChangedCallback(MaxSpeedProperty_Changed)));

        /// <summary>
        /// Gets or sets the max speed.
        /// </summary>
        /// <value>The max speed.</value>
        public double MaxSpeed
        {
            get { return (double)GetValue(MaxSpeedProperty); }
            set { SetValue(MaxSpeedProperty, value); }
        }

        /// <summary>
        /// Maxes the speed property_ changed.
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="e">The <see cref="System.Windows.DependencyPropertyChangedEventArgs"/> instance containing the event data.</param>
        private static void MaxSpeedProperty_Changed(DependencyObject sender, DependencyPropertyChangedEventArgs e)
        {
            InertialPagedScrollViewer control = sender as InertialPagedScrollViewer;

            if (control != null)
                control.OnMaxSpeedChanged((double)e.NewValue);
        }

        /// <summary>
        /// Called when [max speed changed].
        /// </summary>
        /// <param name="maxSpeed">The max speed.</param>
        protected virtual void OnMaxSpeedChanged(double maxSpeed)
        {
            this.Sampler = new SpeedSampler(maxSpeed);
        }

        #endregion

        #region EasingFunctionProperty

        public static readonly DependencyProperty EasingFunctionProperty =
            DependencyProperty.Register(
                "EasingFunction",
                typeof(IEasingFunction),
                typeof(InertialPagedScrollViewer),
                new PropertyMetadata(
                    new QuadraticEase(),
                    new PropertyChangedCallback(EasingFunctionProperty_Changed)));

        /// <summary>
        /// Gets or sets the max speed.
        /// </summary>
        /// <value>The max speed.</value>
        public IEasingFunction EasingFunction
        {
            get { return (IEasingFunction)GetValue(EasingFunctionProperty); }
            set { SetValue(EasingFunctionProperty, value); }
        }

        /// <summary>
        /// Maxes the speed property_ changed.
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="e">The <see cref="System.Windows.DependencyPropertyChangedEventArgs"/> instance containing the event data.</param>
        private static void EasingFunctionProperty_Changed(DependencyObject sender, DependencyPropertyChangedEventArgs e)
        {
            InertialPagedScrollViewer control = sender as InertialPagedScrollViewer;

            if (control != null)
                control.OnEasingFunctionChanged((IEasingFunction)e.NewValue);
        }

        /// <summary>
        /// Called when [max speed changed].
        /// </summary>
        /// <param name="maxSpeed">The max speed.</param>
        protected virtual void OnEasingFunctionChanged(IEasingFunction easingFunction)
        {
            if (this.EasingAnimation != null)
                this.EasingAnimation.EasingFunction = easingFunction;
        }

        #endregion

        #region Fields
        private int _deferredDelta = 0;
        #endregion

        #region Properties

        /// <summary>
        /// Gets or sets the sampler.
        /// </summary>
        /// <value>The sampler.</value>
        private SpeedSampler Sampler { get; set; }

        /// <summary>
        /// Gets or sets a value indicating whether this instance is captured.
        /// </summary>
        /// <value>
        /// 	<c>true</c> if this instance is captured; otherwise, <c>false</c>.
        /// </value>
        private bool IsCaptured { get; set; }

        #endregion

        #region Control

        /// <summary>
        /// Initializes a new instance of the <see cref="InertialPagedScrollViewer"/> class.
        /// </summary>
        public InertialPagedScrollViewer()
        {
            this.DefaultStyleKey = typeof(InertialPagedScrollViewer);
            this.Sampler = new SpeedSampler(this.MaxSpeed);

            this.Loaded += (x, y) =>
            {
                if (_deferredDelta != 0)
                {
                    ScrollDelta(_deferredDelta);
                    _deferredDelta = 0;
                }
            };
        }

        /// <summary>
        /// When overridden in a derived class, is invoked whenever application code or internal processes (such as a rebuilding layout pass) call <see cref="M:System.Windows.Controls.Control.ApplyTemplate"/>.
        /// </summary>
        public override void OnApplyTemplate()
        {
            // Border
            this.RootElement = GetTemplateChild(InertialPagedScrollViewer.RootElementName) as FrameworkElement;

            // Canvas
            this.CanvasElement = GetTemplateChild(InertialPagedScrollViewer.CanvasElementName) as Canvas;
            this.CanvasElement.SizeChanged += new SizeChangedEventHandler(CanvasElement_SizeChanged);

            // clip the content to the Width and Height
            this.CanvasElement.Clip =
                this.CanvasClipping = new RectangleGeometry
                {
                    Rect = new Rect(0, 0, this.CanvasElement.ActualWidth, this.CanvasElement.ActualHeight)
                };

            // ContentControl
            this.ContentElement = GetTemplateChild(InertialPagedScrollViewer.ContentElementName) as ContentControl;
            this.ContentElement.RenderTransform =
                this.Translate = new TranslateTransform { X = 0, Y = 0 };

            this.ContentElement.MouseLeftButtonDown += (a, b) => this.HandleMouseDown(this.GetMousePosition(b, this.Orientation));
            this.ContentElement.MouseLeftButtonUp += (a, b) => this.HandleMouseUp(this.GetMousePosition(b, this.Orientation));
            this.ContentElement.MouseMove += (a, b) => this.HandleMouseMove(this.GetMousePosition(b, this.Orientation));

            // StoryBoard
            this.EasingStoryboard = this.RootElement.Resources[InertialPagedScrollViewer.EasingStoryboardName] as Storyboard;
            this.EasingAnimation = this.EasingStoryboard.Children.OfType<DoubleAnimation>().FirstOrDefault();

            // let do some inizialization
            this.OnOrientationChanged(this.Orientation);
            this.OnEasingFunctionChanged(this.EasingFunction);

            // and call the base class
            base.OnApplyTemplate();
        }

        #endregion

        #region Event Handlers

        /// <summary>
        /// Handles the mouse down.
        /// </summary>
        /// <param name="mousePosition">The mouse position.</param>
        private void HandleMouseDown(double mousePosition)
        {
            if (this.GetBoundary(this.Orientation) >= 0) return;

            this.IsCaptured = true;
            this.ContentElement.CaptureMouse();

            if (this.EasingStoryboard != null)
            {
                double temp = this.GetTranslatePosition(this.Translate, this.Orientation);
                this.EasingStoryboard.Stop();
                this.SetTranslatePosition(this.Translate, this.Orientation, temp);
            }

            this.Sampler.Start(mousePosition);
        }

        /// <summary>
        /// Handles the mouse up.
        /// </summary>
        /// <param name="mousePosition">The mouse position.</param>
        private void HandleMouseUp(double mousePosition)
        {
            if (!UsePaging)
            {
                if (this.GetBoundary(this.Orientation) >= 0) return;

                this.IsCaptured = false;
                this.ContentElement.ReleaseMouseCapture();
                double speed = this.Sampler.GetSpeed();

                if (this.EasingStoryboard != null && this.EasingAnimation != null)
                {
                    this.EasingAnimation.To = this.ComputeNextPosition(Math.Pow(speed, 2) / (20 * this.FrictionFactor) / 1000 * this.Sampler.Direction);
                    this.EasingAnimation.Duration = TimeSpan.FromSeconds(-(speed / 1000) / -this.Deceleration);
                    this.EasingStoryboard.Begin();
                }
            }
            else
            {
                double actualPosition = PrepareScroll();
                int direction = -Math.Sign(GetPageOffset(actualPosition));
                ScrollTo(direction);
            }
        }

        /// <summary>
        /// Handles the mouse move.
        /// </summary>
        /// <param name="mousePosition">The mouse position.</param>
        private void HandleMouseMove(double mousePosition)
        {
            if (this.IsCaptured)
            {
                double position = mousePosition;
                this.SetTranslatePosition(this.Translate, this.Orientation, this.ComputeNextPosition(position - this.Sampler.LastValue));
                this.Sampler.Sample(position);
            }
        }

        /// <summary>
        /// Handles the SizeChanged event of the CanvasElement control.
        /// </summary>
        /// <param name="sender">The source of the event.</param>
        /// <param name="e">The <see cref="System.Windows.SizeChangedEventArgs"/> instance containing the event data.</param>
        void CanvasElement_SizeChanged(object sender, SizeChangedEventArgs e)
        {
            if (this.CanvasClipping != null)
                this.CanvasClipping.Rect =
                    new Rect(0, 0, this.CanvasElement.ActualWidth, this.CanvasElement.ActualHeight);

            if (this.ContentElement != null && this.Orientation == Orientation.Vertical)
                this.ContentElement.Width = this.CanvasElement.ActualWidth;
            else if (this.ContentElement != null && this.Orientation == Orientation.Horizontal)
                this.ContentElement.Height = this.CanvasElement.ActualHeight;

            if (this.GetBoundary(this.Orientation) < 0)
            {
                double position = this.GetTranslatePosition(this.Translate, this.Orientation);

                if (position < this.GetBoundary(this.Orientation))
                    this.SetTranslatePosition(this.Translate, this.Orientation, this.GetBoundary(this.Orientation));
                if (position > 0)
                    this.SetTranslatePosition(this.Translate, this.Orientation, 0);
            }

            this.CanvasElement.UpdateLayout();

            PageWidth = CanvasElement.ActualWidth - (RootElement as Border).Padding.Left - (RootElement as Border).Padding.Right;
        }

        #endregion

        #region Utility

        public void ScrollRight(int pageCount)
        {
            double actualPosition = PrepareScroll();
            double offset = CanvasElement.ActualWidth / 2 * pageCount;
            double newPosition = actualPosition - offset;

            if ((CanvasElement.ActualWidth * pageCount) - actualPosition + (CanvasElement.ActualWidth / 6) >= ContentElement.ActualWidth)
            {
                if (PageLoop)
                    newPosition = CanvasElement.ActualWidth / 2;
                else
                    return;
            }

            this.SetTranslatePosition(this.Translate, this.Orientation, newPosition - 10);
            ScrollTo(-1);
        }

        public void ScrollLeft(int pageCount)
        {
            double actualPosition = PrepareScroll();
            double offset = actualPosition + (CanvasElement.ActualWidth / 2 * pageCount) + 10;

            if (offset >= 0)
            {
                if (PageLoop)
                    offset = (-CanvasElement.ActualWidth * PageCount) + (CanvasElement.ActualWidth / 6);
                else
                    return;
            }

            this.SetTranslatePosition(this.Translate, this.Orientation, offset);
            ScrollTo(1);
        }

        /// <summary>
        /// Gets the position.
        /// </summary>
        /// <param name="mouseArgs">The <see cref="System.Windows.Input.MouseEventArgs"/> instance containing the event data.</param>
        /// <param name="orientation">The orientation.</param>
        /// <returns></returns>
        private double GetMousePosition(MouseEventArgs mouseArgs, Orientation orientation)
        {
            if (orientation == Orientation.Horizontal)
                return mouseArgs.GetPosition(this.RootElement).X;

            return mouseArgs.GetPosition(this.RootElement).Y;
        }

        /// <summary>
        /// Extracts the translate position.
        /// </summary>
        /// <param name="translate">The translate.</param>
        /// <returns></returns>
        private double GetTranslatePosition(TranslateTransform translate, Orientation orientation)
        {
            if (orientation == Orientation.Horizontal)
                return translate.X;

            return translate.Y;
        }

        /// <summary>
        /// Gets the boundary.
        /// </summary>
        /// <param name="orientation">The orientation.</param>
        /// <returns></returns>
        private double GetBoundary(Orientation orientation)
        {
            if (this.Orientation == Orientation.Horizontal)
                return -this.ContentElement.ActualWidth + this.CanvasElement.ActualWidth;

            return -this.ContentElement.ActualHeight + this.CanvasElement.ActualHeight;
        }

        /// <summary>
        /// Extracts the translate position.
        /// </summary>
        /// <param name="translate">The translate.</param>
        /// <returns></returns>
        private void SetTranslatePosition(TranslateTransform translate, Orientation orientation, double value)
        {
            if (orientation == Orientation.Horizontal)
                translate.X = value;
            else
                translate.Y = value;
        }

        /// <summary>
        /// Computes the next position.
        /// </summary>
        /// <param name="speed">The speed.</param>
        /// <returns></returns>
        private double ComputeNextPosition(double speed)
        {
            double newPos = this.GetTranslatePosition(this.Translate, this.Orientation) + speed;

            if (newPos > 0)
                newPos = 0;
            else if (newPos < this.GetBoundary(this.Orientation))
                newPos = this.GetBoundary(this.Orientation);

            return newPos;
        }

        private double GetPageOffset(double translatePosition)
        {
            return -(PageIndex * CanvasElement.ActualWidth) - translatePosition;
        }

        private double PrepareScroll()
        {
            if (this.GetBoundary(this.Orientation) >= 0) return 0;

            this.IsCaptured = false;
            this.ContentElement.ReleaseMouseCapture();

            double actualPosition = this.GetTranslatePosition(this.Translate, this.Orientation);

            if (this.EasingStoryboard != null)
            {
                this.EasingStoryboard.Stop();
                this.SetTranslatePosition(this.Translate, this.Orientation, actualPosition);
            }

            return actualPosition;
        }

        private void ScrollDelta(int delta)
        {
            if (delta < 0) ScrollRight(Math.Abs(delta));
            if (delta > 0) ScrollLeft(Math.Abs(delta));
        }

        private void ScrollTo(int direction)
        {
            double actualPosition = this.GetTranslatePosition(this.Translate, this.Orientation);
            double nextPage = GetNextPageNext(direction, actualPosition);
            if (nextPage < 0 || nextPage >= PageCount) return;

            double speed = nextPage * CanvasElement.ActualWidth + actualPosition;
            Sampler.Start(0);

            if (this.EasingStoryboard != null && this.EasingAnimation != null)
            {
                this.EasingAnimation.To = this.ComputeNextPosition(-speed);
                this.EasingAnimation.Duration = TimeSpan.FromSeconds(Math.Abs((speed / 100) / this.Deceleration));
                this.EasingStoryboard.Begin();
            }

            _pageIndex = (int)nextPage;
            PageIndex = _pageIndex;
        }

        private double GetNextPageNext(int direction, double actualPosition)
        {
            double pageIndex = PageIndex;
            if (Math.Abs(GetPageOffset(actualPosition)) >= (CanvasElement.ActualWidth / 2))
            {
                double pageValue = -actualPosition / CanvasElement.ActualWidth;
                pageIndex = direction < 0 ? Math.Ceiling(pageValue) : Math.Floor(pageValue);
            }

            return pageIndex;
        }

        #endregion
    }
}
